<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) 2023 https://inphp.cc All rights reserved.
// | Licensed ( https://opensource.org/licenses/MIT )
// | Author: 幺月儿(https://gitee.com/lulanyin) Email: inphp@qq.com
// +----------------------------------------------------------------------
// | PDO连接对象
// +----------------------------------------------------------------------
namespace Inphp\Core\Db\PDO;

use Inphp\Core\Db\ConnectionPool;
use Inphp\Core\Db\Db;
use Inphp\Core\Service;
use PDO;
use PDOException;

class Connection
{
    /**
     * 数据库类型
     * @var string
     */
    public string $driver = "mysql";

    /**
     * 超时时长
     * @var int
     */
    private int $timeout = 3;

    /**
     * 字符集
     * @var string
     */
    private string $charset = "utf8mb4";

    /**
     * 保留关键字
     * @var array 
     */
    public array $keywords = [
        "select", "update", "delete", "set", "from", "find_in_set", "where", "join", "in", "on",
        "rank"
    ];

    /**
     * 包含读写的配置
     * @var array
     */
    private array $config = [];

    /**
     * 读写连接池
     * @var ConnectionPool[]|PDO[]
     */
    private array $pool = [
        //读
        "read"  => null,
        //写
        "write" => null
    ];

    /**
     * 连接池长度
     * @var int
     */
    private int $poolLength = 32;

    /**
     * 初始化
     * @param array $configs
     */
    public function __construct(array $configs)
    {
        //数据库类型
        $this->driver = $configs["driver"] ?? "mysql";
        //超时
        $timeout = $configs["timeout"] ?? 3;
        $this->timeout = is_numeric($timeout) && $timeout > 0 && $timeout <= 5 ? ceil($timeout) : 3;
        //字符集
        $this->charset = $configs["charset"] ?? "utf8mb4";
        //保留的关键字
        $this->keywords = is_array($configs["keywords"]) ? array_merge($this->keywords, $configs["keywords"]) : $this->keywords;
        //连接池长度
        $poolLength = $configs["pools"];
        $this->poolLength = is_numeric($poolLength) && $poolLength > 0 ? ceil($poolLength) : 32;
        //配置
        //读
        $read = $configs[Db::READ] ?? [];
        $read = !empty($read) ? $read : ($configs['default'] ?? []);
        if(!empty($read))
        {
            $this->withConfig($read)->connect();
        }
        //写
        $write = $configs[Db::WRITE] ?? [];
        $write = !empty($write) ? $write : ($configs['default'] ?? []);
        if(!empty($write))
        {
            $this->withConfig($write, Db::WRITE)->connect(Db::WRITE);
        }
        //如果启用连接池，需要在系统运行时他那长连接
        if (Service::isCLI()) {
            $this->connect();
            $this->connect(Db::WRITE);
        }
    }

    /**
     * 对连接进行单独配置
     * @param array $config
     * @param string $type
     * @return Connection
     */
    public function withConfig(array $config, string $type = Db::READ): Connection
    {
        $this->config[$type == Db::WRITE ? Db::WRITE : Db::READ] = $config;
        return $this;
    }

    /**
     * 新建一个PDO连接对象
     * @param string $type
     * @return PDO|null
     */
    public function newPDO(string $type = Db::READ): ?PDO
    {
        $config = $this->config[$type] ?? [];
        $driver = $this->driver;
        $host = $config["host"] ?? "127.0.0.1";
        $port = $config["port"] ?? 3306;
        $dbname = $config["dbname"] ?? "";
        $username = $config["username"] ?? "";
        $password = $config["password"] ?? "";
        $options = [
            //长连接
            PDO::ATTR_PERSISTENT => true,
            //预编译
            PDO::ATTR_EMULATE_PREPARES => true,
            //使用try catch捕获异常
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            //超时
            PDO::ATTR_TIMEOUT => $this->timeout
        ];
        if (is_array($config["options"]) && !empty($config["options"])) {
            foreach ($config["options"] as $key => $val) {
                $options[$key] = $val;
            }
        }
        try {
            return new PDO(
                "{$driver}:host={$host};port={$port};dbname={$dbname}".($driver !== "pgsql" ? ";charset={$this->charset}" : ""),
                $username,
                $password,
                $options
            );
        } catch (PDOException $exception) {
            //数据库连接失败，记录日志
            Db::saveErrorLog($exception);
            return null;
        }
    }

    /**
     * 连接
     */
    private function connect(string $type = Db::READ): void
    {
        if (Service::isCLI()) {
            //使用连接池
            $this->pool[$type] = new ConnectionPool(function () use($type) {
                return $this->newPDO($type);
            }, $this->poolLength);
        } else {
            $this->pool[$type] = $this->newPDO($type);
        }
    }

    /**
     * 重连
     * @param string|null $type
     * @return PDO
     */
    public function reconnect(?string $type = null): PDO
    {
        Db::saveErrorLog("正在尝试重连...");
        return $this->newPDO($type);
    }

    /**
     * 主动关闭mysql连接
     * 仅适用于非swoole环境
     * @param string|null $type
     */
    public function disconnect(?string $type = null)
    {
        if ((is_null($type) || $type === Db::READ) && isset($this->pool[Db::READ])) {
            $this->pool[Db::READ] = null;
        }
        if ((is_null($type) || $type === Db::WRITE) && isset($this->pool[Db::WRITE])) {
            $this->pool[Db::WRITE] = null;
        }
    }

    /**
     * 获取PDO对象
     * @param string $type
     * @return PDO
     */
    public function get(string $type = Db::READ): PDO
    {
        //优先从上下文中获取
        if ($pdo = Db::inTransaction()) {
            Db::debug("正在事务:{$type}，直接从上下文获取PDO对象");
            return $pdo;
        }
        Db::debug("在连接池取:{$type} PDO对象");
        $pdo = Service::isCLI() ? $this->pool[$type]?->get() : $this->pool[$type];
        if (is_null($pdo)) {
            //防止未初始化
            $pdo = $this->newPDO($type);
        }
        return $pdo;
        /* 断线重连使用查询时重连，在此处使用将带来额外的消耗
        try {
            $pdo->getAttribute(PDO::ATTR_SERVER_INFO);
            return $pdo;
        } catch (PDOException $exception) {
            if (stripos(strtolower($exception->getMessage()), "server has gone away") !== false) {
                //断开连接了，需要重连
                return $this->reconnect($type);
            }
            return null;
        }
        */
    }

    /**
     * 回收PDO对象
     * @param PDO $PDO
     * @param string $type
     */
    public function put(PDO $PDO, string $type = Db::READ): void
    {
        if (Service::isCLI()) {
            if (!Db::inTransaction()) {
                Db::debug("回收:{$type} PDO对象成功");
                $this->pool[$type]?->put($PDO);
            } else {
                Db::debug("正在执行事务，不可回收:{$type} PDO对象");
            }
        }
    }

    /**
     * 获取配置
     * @param string $type
     * @return array
     */
    public function getConfig(string $type = Db::READ): array
    {
        return $this->config[$type] ?? [];
    }

    /**
     * 获取数据库名
     * @param string $type
     * @return string
     */
    public function getDatabaseName(string $type = Db::READ): string
    {
        return $this->getConfig($type)["dbname"] ?? "";
    }

    /**
     * 转化关键字
     * @param mixed $fields
     * @return string
     */
    public function parseColumns(mixed $fields) : string
    {
        $fields = is_array($fields) ? explode(",", join(",", $fields)) : explode(",", $fields);
        $list = [];
        foreach ($fields as $field)
        {
            $field = trim($field);
            if(in_array($field, $this->keywords))
            {
                $field = "`{$field}`";
            }
            $list[] = $field;
        }
        return join(",", $list);
    }
}